/**
 * Music editor
 *
 * @author pquiring
 *
 * Created : Jan 14, 2014
 */

import java.util.*;
import java.io.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;
import javax.swing.filechooser.*;
import javax.sound.midi.*;

import javaforce.*;
import javaforce.media.*;
import javaforce.jni.*;

public class SongPanel extends javax.swing.JPanel implements Music.Listener, Receiver, MediaIO {

  /**
   * Creates new form MainPanel
   */
  public SongPanel() {
    initComponents();
    tableView.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
    tableView.setTableHeader(null);
    tableView.setRowHeight(20);
    colModel = (DefaultTableColumnModel)tableView.getColumnModel();
    samples = new Samples(this);
    samplesPane.setViewportView(samples);
    loadLibrary();
    reset();
  }

  /**
   * This method is called from within the constructor to initialize the form. WARNING: Do NOT modify this code. The content of this method is always regenerated by the Form
   * Editor.
   */
  @SuppressWarnings("unchecked")
  // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
  private void initComponents() {

    noteTypes = new javax.swing.ButtonGroup();
    tabs = new javax.swing.JTabbedPane();
    generalTab = new javax.swing.JPanel();
    jLabel1 = new javax.swing.JLabel();
    name = new javax.swing.JTextField();
    jLabel8 = new javax.swing.JLabel();
    jScrollPane3 = new javax.swing.JScrollPane();
    comment = new javax.swing.JTextArea();
    patternsTab = new javax.swing.JPanel();
    jToolBar2 = new javax.swing.JToolBar();
    jLabel11 = new javax.swing.JLabel();
    pattern = new javax.swing.JComboBox();
    addPattern = new javax.swing.JButton();
    removePattern = new javax.swing.JButton();
    jButton10 = new javax.swing.JButton();
    filler2 = new javax.swing.Box.Filler(new java.awt.Dimension(10, 0), new java.awt.Dimension(10, 0), new java.awt.Dimension(10, 32767));
    jButton11 = new javax.swing.JButton();
    jButton12 = new javax.swing.JButton();
    filler5 = new javax.swing.Box.Filler(new java.awt.Dimension(10, 0), new java.awt.Dimension(10, 0), new java.awt.Dimension(10, 32767));
    jButton2 = new javax.swing.JButton();
    jButton3 = new javax.swing.JButton();
    jButton1 = new javax.swing.JButton();
    jToolBar4 = new javax.swing.JToolBar();
    jLabel2 = new javax.swing.JLabel();
    bpm = new javax.swing.JSpinner();
    filler3 = new javax.swing.Box.Filler(new java.awt.Dimension(10, 0), new java.awt.Dimension(10, 0), new java.awt.Dimension(10, 32767));
    jLabel3 = new javax.swing.JLabel();
    jButton7 = new javax.swing.JButton();
    jButton8 = new javax.swing.JButton();
    filler1 = new javax.swing.Box.Filler(new java.awt.Dimension(10, 0), new java.awt.Dimension(10, 0), new java.awt.Dimension(10, 32767));
    jScrollPane1 = new javax.swing.JScrollPane();
    tableView = new javax.swing.JTable() {
      public TableCellEditor getCellEditor(int row, int column) {
        if (row == 0) return _getCellEditor(row, column);
        return super.getCellEditor(row, column);
      }
    };
    seqTab = new javax.swing.JPanel();
    jScrollPane4 = new javax.swing.JScrollPane();
    sequence = new javax.swing.JList();
    seqUp = new javax.swing.JButton();
    seqDown = new javax.swing.JButton();
    jToolBar1 = new javax.swing.JToolBar();
    jLabel13 = new javax.swing.JLabel();
    seqPattern = new javax.swing.JComboBox();
    seqAdd = new javax.swing.JButton();
    seqRemove = new javax.swing.JButton();
    instrTab = new javax.swing.JPanel();
    jToolBar5 = new javax.swing.JToolBar();
    jLabel14 = new javax.swing.JLabel();
    instrument = new javax.swing.JComboBox();
    iRemove = new javax.swing.JButton();
    jButton9 = new javax.swing.JButton();
    filler9 = new javax.swing.Box.Filler(new java.awt.Dimension(10, 0), new java.awt.Dimension(10, 0), new java.awt.Dimension(10, 32767));
    jLabel5 = new javax.swing.JLabel();
    library = new javax.swing.JComboBox();
    iAddLibrary = new javax.swing.JButton();
    filler10 = new javax.swing.Box.Filler(new java.awt.Dimension(10, 0), new java.awt.Dimension(10, 0), new java.awt.Dimension(10, 32767));
    jLabel7 = new javax.swing.JLabel();
    iAddFile = new javax.swing.JButton();
    filler6 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 0));
    jToolBar7 = new javax.swing.JToolBar();
    jLabel6 = new javax.swing.JLabel();
    regions = new javax.swing.JComboBox();
    jButton14 = new javax.swing.JButton();
    jButton15 = new javax.swing.JButton();
    jButton13 = new javax.swing.JButton();
    filler11 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 0));
    samplesTab = new javax.swing.JPanel();
    jToolBar3 = new javax.swing.JToolBar();
    jLabel9 = new javax.swing.JLabel();
    sample = new javax.swing.JComboBox();
    jButton17 = new javax.swing.JButton();
    jButton18 = new javax.swing.JButton();
    jButton16 = new javax.swing.JButton();
    filler8 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 0));
    jToolBar6 = new javax.swing.JToolBar();
    sustain = new javax.swing.JCheckBox();
    loop = new javax.swing.JCheckBox();
    attenuation = new javax.swing.JCheckBox();
    filler4 = new javax.swing.Box.Filler(new java.awt.Dimension(10, 0), new java.awt.Dimension(10, 0), new java.awt.Dimension(10, 32767));
    jLabel4 = new javax.swing.JLabel();
    zoom = new javax.swing.JComboBox();
    jButton4 = new javax.swing.JButton();
    jButton5 = new javax.swing.JButton();
    jButton6 = new javax.swing.JButton();
    filler7 = new javax.swing.Box.Filler(new java.awt.Dimension(0, 0), new java.awt.Dimension(0, 0), new java.awt.Dimension(32767, 0));
    samplesPane = new javax.swing.JScrollPane();

    tabs.setPreferredSize(new java.awt.Dimension(418, 580));

    generalTab.setPreferredSize(new java.awt.Dimension(418, 580));

    jLabel1.setText("Name:");

    jLabel8.setText("Comments:");

    comment.setColumns(20);
    comment.setRows(5);
    jScrollPane3.setViewportView(comment);

    javax.swing.GroupLayout generalTabLayout = new javax.swing.GroupLayout(generalTab);
    generalTab.setLayout(generalTabLayout);
    generalTabLayout.setHorizontalGroup(
      generalTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(generalTabLayout.createSequentialGroup()
        .addContainerGap()
        .addGroup(generalTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
          .addGroup(generalTabLayout.createSequentialGroup()
            .addComponent(jLabel1)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(name))
          .addGroup(generalTabLayout.createSequentialGroup()
            .addComponent(jLabel8)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(jScrollPane3, javax.swing.GroupLayout.DEFAULT_SIZE, 550, Short.MAX_VALUE)))
        .addContainerGap())
    );
    generalTabLayout.setVerticalGroup(
      generalTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(generalTabLayout.createSequentialGroup()
        .addContainerGap()
        .addGroup(generalTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
          .addComponent(jLabel1)
          .addComponent(name, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
        .addGroup(generalTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
          .addComponent(jLabel8)
          .addComponent(jScrollPane3, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE))
        .addContainerGap(449, Short.MAX_VALUE))
    );

    tabs.addTab("General", generalTab);

    patternsTab.setPreferredSize(new java.awt.Dimension(418, 580));

    jToolBar2.setFloatable(false);
    jToolBar2.setRollover(true);

    jLabel11.setText("Pattern:");
    jToolBar2.add(jLabel11);

    pattern.setMaximumSize(new java.awt.Dimension(128, 20));
    pattern.setMinimumSize(new java.awt.Dimension(128, 20));
    pattern.addItemListener(new java.awt.event.ItemListener() {
      public void itemStateChanged(java.awt.event.ItemEvent evt) {
        patternItemStateChanged(evt);
      }
    });
    jToolBar2.add(pattern);

    addPattern.setIcon(new javax.swing.ImageIcon(getClass().getResource("/plus.png"))); // NOI18N
    addPattern.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        addPatternActionPerformed(evt);
      }
    });
    jToolBar2.add(addPattern);

    removePattern.setIcon(new javax.swing.ImageIcon(getClass().getResource("/minus.png"))); // NOI18N
    removePattern.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        removePatternActionPerformed(evt);
      }
    });
    jToolBar2.add(removePattern);

    jButton10.setIcon(new javax.swing.ImageIcon(getClass().getResource("/edit.png"))); // NOI18N
    jButton10.setFocusable(false);
    jButton10.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    jButton10.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    jButton10.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton10ActionPerformed(evt);
      }
    });
    jToolBar2.add(jButton10);
    jToolBar2.add(filler2);

    jButton11.setIcon(new javax.swing.ImageIcon(getClass().getResource("/up.png"))); // NOI18N
    jButton11.setFocusable(false);
    jButton11.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    jButton11.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    jButton11.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton11ActionPerformed(evt);
      }
    });
    jToolBar2.add(jButton11);

    jButton12.setIcon(new javax.swing.ImageIcon(getClass().getResource("/down.png"))); // NOI18N
    jButton12.setFocusable(false);
    jButton12.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    jButton12.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    jButton12.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton12ActionPerformed(evt);
      }
    });
    jToolBar2.add(jButton12);
    jToolBar2.add(filler5);

    jButton2.setIcon(new javax.swing.ImageIcon(getClass().getResource("/play.png"))); // NOI18N
    jButton2.setFocusable(false);
    jButton2.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    jButton2.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    jButton2.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton2ActionPerformed(evt);
      }
    });
    jToolBar2.add(jButton2);

    jButton3.setIcon(new javax.swing.ImageIcon(getClass().getResource("/stop.png"))); // NOI18N
    jButton3.setFocusable(false);
    jButton3.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    jButton3.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    jButton3.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton3ActionPerformed(evt);
      }
    });
    jToolBar2.add(jButton3);

    jButton1.setIcon(new javax.swing.ImageIcon(getClass().getResource("/record.png"))); // NOI18N
    jButton1.setFocusable(false);
    jButton1.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    jButton1.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    jButton1.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton1ActionPerformed(evt);
      }
    });
    jToolBar2.add(jButton1);

    jToolBar4.setFloatable(false);

    jLabel2.setText("BPM:");
    jToolBar4.add(jLabel2);

    bpm.setModel(new javax.swing.SpinnerNumberModel(80, 1, 65536, 1));
    bpm.setMaximumSize(new java.awt.Dimension(60, 20));
    bpm.setMinimumSize(new java.awt.Dimension(20, 20));
    bpm.setPreferredSize(new java.awt.Dimension(20, 20));
    bpm.addChangeListener(new javax.swing.event.ChangeListener() {
      public void stateChanged(javax.swing.event.ChangeEvent evt) {
        bpmStateChanged(evt);
      }
    });
    bpm.addKeyListener(new java.awt.event.KeyAdapter() {
      public void keyTyped(java.awt.event.KeyEvent evt) {
        bpmKeyTyped(evt);
      }
    });
    jToolBar4.add(bpm);
    jToolBar4.add(filler3);

    jLabel3.setText("Track:");
    jToolBar4.add(jLabel3);

    jButton7.setIcon(new javax.swing.ImageIcon(getClass().getResource("/plus.png"))); // NOI18N
    jButton7.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton7ActionPerformed(evt);
      }
    });
    jToolBar4.add(jButton7);

    jButton8.setIcon(new javax.swing.ImageIcon(getClass().getResource("/minus.png"))); // NOI18N
    jButton8.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton8ActionPerformed(evt);
      }
    });
    jToolBar4.add(jButton8);
    jToolBar4.add(filler1);

    tableView.setFont(JFAWT.getMonospacedFont(0, 12));
    tableView.setModel(tableModel);
    tableView.setCellSelectionEnabled(true);
    tableView.getTableHeader().setReorderingAllowed(false);
    tableView.addMouseListener(new java.awt.event.MouseAdapter() {
      public void mouseClicked(java.awt.event.MouseEvent evt) {
        tableViewMouseClicked(evt);
      }
    });
    tableView.addKeyListener(new java.awt.event.KeyAdapter() {
      public void keyPressed(java.awt.event.KeyEvent evt) {
        tableViewKeyPressed(evt);
      }
      public void keyReleased(java.awt.event.KeyEvent evt) {
        tableViewKeyReleased(evt);
      }
      public void keyTyped(java.awt.event.KeyEvent evt) {
        tableViewKeyTyped(evt);
      }
    });
    jScrollPane1.setViewportView(tableView);
    tableView.getColumnModel().getSelectionModel().setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);

    javax.swing.GroupLayout patternsTabLayout = new javax.swing.GroupLayout(patternsTab);
    patternsTab.setLayout(patternsTabLayout);
    patternsTabLayout.setHorizontalGroup(
      patternsTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addComponent(jToolBar2, javax.swing.GroupLayout.DEFAULT_SIZE, 628, Short.MAX_VALUE)
      .addComponent(jToolBar4, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
      .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 628, Short.MAX_VALUE)
    );
    patternsTabLayout.setVerticalGroup(
      patternsTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(patternsTabLayout.createSequentialGroup()
        .addComponent(jToolBar2, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(jToolBar4, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
        .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 520, Short.MAX_VALUE))
    );

    tabs.addTab("Patterns", patternsTab);

    seqTab.setPreferredSize(new java.awt.Dimension(418, 580));

    sequence.addListSelectionListener(new javax.swing.event.ListSelectionListener() {
      public void valueChanged(javax.swing.event.ListSelectionEvent evt) {
        sequenceValueChanged(evt);
      }
    });
    jScrollPane4.setViewportView(sequence);

    seqUp.setIcon(new javax.swing.ImageIcon(getClass().getResource("/up.png"))); // NOI18N
    seqUp.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        seqUpActionPerformed(evt);
      }
    });

    seqDown.setIcon(new javax.swing.ImageIcon(getClass().getResource("/down.png"))); // NOI18N
    seqDown.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        seqDownActionPerformed(evt);
      }
    });

    jToolBar1.setFloatable(false);
    jToolBar1.setRollover(true);

    jLabel13.setText("Pattern:");
    jToolBar1.add(jLabel13);

    seqPattern.setMaximumSize(new java.awt.Dimension(128, 20));
    seqPattern.setMinimumSize(new java.awt.Dimension(128, 20));
    jToolBar1.add(seqPattern);

    seqAdd.setIcon(new javax.swing.ImageIcon(getClass().getResource("/plus.png"))); // NOI18N
    seqAdd.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        seqAddActionPerformed(evt);
      }
    });
    jToolBar1.add(seqAdd);

    seqRemove.setIcon(new javax.swing.ImageIcon(getClass().getResource("/minus.png"))); // NOI18N
    seqRemove.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        seqRemoveActionPerformed(evt);
      }
    });

    javax.swing.GroupLayout seqTabLayout = new javax.swing.GroupLayout(seqTab);
    seqTab.setLayout(seqTabLayout);
    seqTabLayout.setHorizontalGroup(
      seqTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(seqTabLayout.createSequentialGroup()
        .addContainerGap()
        .addComponent(jScrollPane4, javax.swing.GroupLayout.PREFERRED_SIZE, 172, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addGroup(seqTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
          .addComponent(seqUp)
          .addComponent(seqDown)
          .addComponent(seqRemove))
        .addContainerGap(391, Short.MAX_VALUE))
      .addComponent(jToolBar1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
    );
    seqTabLayout.setVerticalGroup(
      seqTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(seqTabLayout.createSequentialGroup()
        .addComponent(jToolBar1, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addGroup(seqTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
          .addGroup(seqTabLayout.createSequentialGroup()
            .addComponent(seqUp)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(seqDown)
            .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
            .addComponent(seqRemove)
            .addGap(0, 0, Short.MAX_VALUE))
          .addComponent(jScrollPane4, javax.swing.GroupLayout.DEFAULT_SIZE, 556, Short.MAX_VALUE)))
    );

    tabs.addTab("Sequence", seqTab);

    instrTab.setPreferredSize(new java.awt.Dimension(418, 580));

    jToolBar5.setFloatable(false);
    jToolBar5.setRollover(true);
    jToolBar5.setMinimumSize(new java.awt.Dimension(1, 1));
    jToolBar5.setPreferredSize(new java.awt.Dimension(1, 25));

    jLabel14.setText("Instrument:");
    jToolBar5.add(jLabel14);

    instrument.setMaximumSize(new java.awt.Dimension(256, 32767));
    instrument.setMinimumSize(new java.awt.Dimension(256, 20));
    instrument.addItemListener(new java.awt.event.ItemListener() {
      public void itemStateChanged(java.awt.event.ItemEvent evt) {
        instrumentItemStateChanged(evt);
      }
    });
    jToolBar5.add(instrument);

    iRemove.setIcon(new javax.swing.ImageIcon(getClass().getResource("/minus.png"))); // NOI18N
    iRemove.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        iRemoveActionPerformed(evt);
      }
    });
    jToolBar5.add(iRemove);

    jButton9.setIcon(new javax.swing.ImageIcon(getClass().getResource("/edit.png"))); // NOI18N
    jButton9.setFocusable(false);
    jButton9.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    jButton9.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    jButton9.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton9ActionPerformed(evt);
      }
    });
    jToolBar5.add(jButton9);
    jToolBar5.add(filler9);

    jLabel5.setText("Library:");
    jToolBar5.add(jLabel5);

    library.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "Piano", "etc" }));
    library.setMaximumSize(new java.awt.Dimension(256, 32767));
    library.setMinimumSize(new java.awt.Dimension(256, 20));
    jToolBar5.add(library);

    iAddLibrary.setIcon(new javax.swing.ImageIcon(getClass().getResource("/plus.png"))); // NOI18N
    iAddLibrary.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        iAddLibraryActionPerformed(evt);
      }
    });
    jToolBar5.add(iAddLibrary);
    jToolBar5.add(filler10);

    jLabel7.setText("Import:");
    jToolBar5.add(jLabel7);

    iAddFile.setIcon(new javax.swing.ImageIcon(getClass().getResource("/plus.png"))); // NOI18N
    iAddFile.setToolTipText("");
    iAddFile.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        iAddFileActionPerformed(evt);
      }
    });
    jToolBar5.add(iAddFile);
    jToolBar5.add(filler6);

    jToolBar7.setFloatable(false);
    jToolBar7.setRollover(true);

    jLabel6.setText("Regions:");
    jToolBar7.add(jLabel6);

    regions.setMaximumSize(new java.awt.Dimension(256, 32767));
    regions.setMinimumSize(new java.awt.Dimension(256, 20));
    regions.addItemListener(new java.awt.event.ItemListener() {
      public void itemStateChanged(java.awt.event.ItemEvent evt) {
        regionsItemStateChanged(evt);
      }
    });
    jToolBar7.add(regions);

    jButton14.setIcon(new javax.swing.ImageIcon(getClass().getResource("/plus.png"))); // NOI18N
    jButton14.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton14ActionPerformed(evt);
      }
    });
    jToolBar7.add(jButton14);

    jButton15.setIcon(new javax.swing.ImageIcon(getClass().getResource("/minus.png"))); // NOI18N
    jButton15.setToolTipText("");
    jButton15.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton15ActionPerformed(evt);
      }
    });
    jToolBar7.add(jButton15);

    jButton13.setIcon(new javax.swing.ImageIcon(getClass().getResource("/edit.png"))); // NOI18N
    jButton13.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton13ActionPerformed(evt);
      }
    });
    jToolBar7.add(jButton13);
    jToolBar7.add(filler11);

    javax.swing.GroupLayout instrTabLayout = new javax.swing.GroupLayout(instrTab);
    instrTab.setLayout(instrTabLayout);
    instrTabLayout.setHorizontalGroup(
      instrTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addComponent(jToolBar5, javax.swing.GroupLayout.DEFAULT_SIZE, 628, Short.MAX_VALUE)
      .addComponent(jToolBar7, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
    );
    instrTabLayout.setVerticalGroup(
      instrTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(instrTabLayout.createSequentialGroup()
        .addComponent(jToolBar5, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(jToolBar7, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addContainerGap(531, Short.MAX_VALUE))
    );

    tabs.addTab("Instruments", instrTab);

    jToolBar3.setFloatable(false);
    jToolBar3.setRollover(true);

    jLabel9.setText("Sample:");
    jToolBar3.add(jLabel9);

    sample.setMaximumSize(new java.awt.Dimension(256, 32767));
    sample.setMinimumSize(new java.awt.Dimension(256, 20));
    sample.addItemListener(new java.awt.event.ItemListener() {
      public void itemStateChanged(java.awt.event.ItemEvent evt) {
        sampleItemStateChanged(evt);
      }
    });
    jToolBar3.add(sample);

    jButton17.setIcon(new javax.swing.ImageIcon(getClass().getResource("/plus.png"))); // NOI18N
    jButton17.setToolTipText("");
    jButton17.setFocusable(false);
    jButton17.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    jButton17.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    jButton17.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton17ActionPerformed(evt);
      }
    });
    jToolBar3.add(jButton17);

    jButton18.setIcon(new javax.swing.ImageIcon(getClass().getResource("/minus.png"))); // NOI18N
    jButton18.setFocusable(false);
    jButton18.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    jButton18.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    jButton18.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton18ActionPerformed(evt);
      }
    });
    jToolBar3.add(jButton18);

    jButton16.setIcon(new javax.swing.ImageIcon(getClass().getResource("/edit.png"))); // NOI18N
    jButton16.setFocusable(false);
    jButton16.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
    jButton16.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
    jButton16.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton16ActionPerformed(evt);
      }
    });
    jToolBar3.add(jButton16);
    jToolBar3.add(filler8);

    jToolBar6.setFloatable(false);
    jToolBar6.setRollover(true);
    jToolBar6.setMinimumSize(new java.awt.Dimension(1, 1));
    jToolBar6.setPreferredSize(new java.awt.Dimension(1, 25));

    sustain.setText("Sustain");
    sustain.addItemListener(new java.awt.event.ItemListener() {
      public void itemStateChanged(java.awt.event.ItemEvent evt) {
        sustainItemStateChanged(evt);
      }
    });
    jToolBar6.add(sustain);

    loop.setText("Loop");
    loop.addItemListener(new java.awt.event.ItemListener() {
      public void itemStateChanged(java.awt.event.ItemEvent evt) {
        loopItemStateChanged(evt);
      }
    });
    jToolBar6.add(loop);

    attenuation.setText("Attenuation");
    attenuation.addItemListener(new java.awt.event.ItemListener() {
      public void itemStateChanged(java.awt.event.ItemEvent evt) {
        attenuationItemStateChanged(evt);
      }
    });
    jToolBar6.add(attenuation);
    jToolBar6.add(filler4);

    jLabel4.setText("Zoom:");
    jToolBar6.add(jLabel4);

    zoom.setModel(new javax.swing.DefaultComboBoxModel(new String[] { "1:1", "1:2", "1:4", "1:8", "1:16", "1:32", "1:64", "1:128" }));
    zoom.setMaximumSize(new java.awt.Dimension(128, 32767));
    zoom.setMinimumSize(new java.awt.Dimension(128, 20));
    zoom.addItemListener(new java.awt.event.ItemListener() {
      public void itemStateChanged(java.awt.event.ItemEvent evt) {
        zoomItemStateChanged(evt);
      }
    });
    jToolBar6.add(zoom);

    jButton4.setIcon(new javax.swing.ImageIcon(getClass().getResource("/play.png"))); // NOI18N
    jButton4.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton4ActionPerformed(evt);
      }
    });
    jToolBar6.add(jButton4);

    jButton5.setIcon(new javax.swing.ImageIcon(getClass().getResource("/stop.png"))); // NOI18N
    jButton5.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton5ActionPerformed(evt);
      }
    });
    jToolBar6.add(jButton5);

    jButton6.setIcon(new javax.swing.ImageIcon(getClass().getResource("/keyup.png"))); // NOI18N
    jButton6.setToolTipText("Key up");
    jButton6.addActionListener(new java.awt.event.ActionListener() {
      public void actionPerformed(java.awt.event.ActionEvent evt) {
        jButton6ActionPerformed(evt);
      }
    });
    jToolBar6.add(jButton6);
    jToolBar6.add(filler7);

    javax.swing.GroupLayout samplesTabLayout = new javax.swing.GroupLayout(samplesTab);
    samplesTab.setLayout(samplesTabLayout);
    samplesTabLayout.setHorizontalGroup(
      samplesTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addComponent(jToolBar3, javax.swing.GroupLayout.DEFAULT_SIZE, 628, Short.MAX_VALUE)
      .addComponent(jToolBar6, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
      .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, samplesTabLayout.createSequentialGroup()
        .addContainerGap()
        .addComponent(samplesPane)
        .addContainerGap())
    );
    samplesTabLayout.setVerticalGroup(
      samplesTabLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addGroup(samplesTabLayout.createSequentialGroup()
        .addComponent(jToolBar3, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(jToolBar6, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
        .addComponent(samplesPane, javax.swing.GroupLayout.DEFAULT_SIZE, 514, Short.MAX_VALUE)
        .addContainerGap())
    );

    tabs.addTab("Samples", samplesTab);

    javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
    this.setLayout(layout);
    layout.setHorizontalGroup(
      layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addComponent(tabs, javax.swing.GroupLayout.DEFAULT_SIZE, 633, Short.MAX_VALUE)
    );
    layout.setVerticalGroup(
      layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
      .addComponent(tabs, javax.swing.GroupLayout.DEFAULT_SIZE, 615, Short.MAX_VALUE)
    );
  }// </editor-fold>//GEN-END:initComponents

  private void addPatternActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addPatternActionPerformed
    music.addPattern(4);
    int size = music.song.patterns.size();
    String name = "Verse " + size;
    int idx = size - 1;
    music.song.patterns.get(idx).name = name;
    pattern.addItem(name);
    pattern.setSelectedIndex(idx);
    updatePattern();
  }//GEN-LAST:event_addPatternActionPerformed

  private void patternItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_patternItemStateChanged
    currentPatternIdx = pattern.getSelectedIndex();
    updatePattern();
  }//GEN-LAST:event_patternItemStateChanged

  private void tableViewMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_tableViewMouseClicked
    evt.consume();
    if (evt.getClickCount() == 2) {
      editNote();
    }
  }//GEN-LAST:event_tableViewMouseClicked

  private void tableViewKeyTyped(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_tableViewKeyTyped
    evt.consume();
    enterNote(evt.getKeyChar());
  }//GEN-LAST:event_tableViewKeyTyped

  private void removePatternActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_removePatternActionPerformed
    if (!JFAWT.showConfirm("Delete Pattern?", "Are you sure?")) return;
    music.removePattern(currentPatternIdx);
    if (music.song.patterns.size() == 0) {
      music.addPattern(4);
    }
    updatePattern();
  }//GEN-LAST:event_removePatternActionPerformed

  private void jButton7ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton7ActionPerformed
    music.addTrack(currentPatternIdx);
    updatePattern();
  }//GEN-LAST:event_jButton7ActionPerformed

  private void jButton8ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton8ActionPerformed
    int col = tableView.getSelectedColumn();
    if (col == -1) {
      JFAWT.showError("Notice", "Select a track to delete");
      return;
    }
    if (col == 0) return;
    if (!JFAWT.showConfirm("Delete Track?", "Are you sure?")) return;
    music.removeTrack(currentPatternIdx, col - 1);
    updatePattern();
  }//GEN-LAST:event_jButton8ActionPerformed

  private void seqAddActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_seqAddActionPerformed
    int idx = seqPattern.getSelectedIndex();
    if (idx == -1) return;
    music.song.sequence.add(idx);
    updateSequenceList();
  }//GEN-LAST:event_seqAddActionPerformed

  private void seqRemoveActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_seqRemoveActionPerformed
    int idx = sequence.getSelectedIndex();
    if (idx == -1) {
      JFAWT.showError("Notice", "Select a sequence to delete");
      return;
    }
//    if (!JFAWT.showConfirm("Delete Sequence?", "Are you sure?")) return;
    music.song.sequence.remove(idx);
    updateSequenceList();
  }//GEN-LAST:event_seqRemoveActionPerformed

  private void seqUpActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_seqUpActionPerformed
    int idx = sequence.getSelectedIndex();
    if (idx <= 0) return;
    Integer top = music.song.sequence.get(idx-1);
    Integer bot = music.song.sequence.get(idx);
    music.song.sequence.set(idx-1, bot);
    music.song.sequence.set(idx, top);
    updateSequenceList();
    sequence.setSelectedIndex(idx-1);
  }//GEN-LAST:event_seqUpActionPerformed

  private void seqDownActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_seqDownActionPerformed
    int idx = sequence.getSelectedIndex();
    if (idx == -1) return;
    if (idx == music.song.sequence.size() - 1) return;
    Integer top = music.song.sequence.get(idx);
    Integer bot = music.song.sequence.get(idx+1);
    music.song.sequence.set(idx, bot);
    music.song.sequence.set(idx+1, top);
    updateSequenceList();
    sequence.setSelectedIndex(idx+1);
  }//GEN-LAST:event_seqDownActionPerformed

  private void tableViewKeyPressed(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_tableViewKeyPressed
    evt.consume();
    int row = tableView.getSelectedRow();
    int col = tableView.getSelectedColumn();
    if (row == -1 || col == -1) return;
    if (row == 0) return;
    TableCell cell = null;
    if (col > 0) cell = (TableCell)tableView.getValueAt(row, col);
    int cnt = -1, sel = -1;
    if (evt.getModifiers() == 0) {
      switch(evt.getKeyCode()) {
        case KeyEvent.VK_ESCAPE:
          if (recording) recordStop();
          break;
        case KeyEvent.VK_DELETE:
          enterNote(' ');
          break;
        case KeyEvent.VK_ENTER:
          editNote();
          break;
        case KeyEvent.VK_UP:
          if (row == 1) break;
          if (col > 0) sel = cell.getSelection();
          row--;
          if (col > 0) ((TableCell)tableView.getValueAt(row, col)).setSelection(sel);
          break;
        case KeyEvent.VK_DOWN:
          if (row == tableView.getRowCount() - 1) break;
          if (col > 0) sel = cell.getSelection();
          row++;
          if (col > 0) ((TableCell)tableView.getValueAt(row, col)).setSelection(sel);
          break;
        case KeyEvent.VK_LEFT:
          if (col == 0) break;
          if (cell == null) break;  //should not happen
          sel = cell.getSelection();
          if (sel == 0) {
            col--;
            if (col > 0) {
              cell = (TableCell)tableView.getValueAt(row, col);
              cell.setSelection(2);
            }
          } else {
            cell.setSelection(sel-1);
          }
          break;
        case KeyEvent.VK_RIGHT:
          cnt = tableView.getColumnCount();
          if (cell == null) {
            if (col == cnt-1) break;
            col++;
            break;
          }
          sel = cell.getSelection();
          if (sel == 2) {
            if (col == cnt-1) break;
            col++;
            cell = (TableCell)tableView.getValueAt(row, col);
            cell.setSelection(0);
          } else {
            cell.setSelection(sel+1);
          }
          break;
      }
    }
    if (col > 1 && evt.getModifiers() == KeyEvent.CTRL_MASK) {
      Music.Track track = music.song.patterns.get(currentPatternIdx).tracks.get(col-1);
      switch(evt.getKeyCode()) {
        case KeyEvent.VK_C:
          //copy note/vol/fx
          cbNote = track.notes[row-1];
          cbVolCmd = track.volcmds[row-1];
          cbVolParam = track.volparams[row-1];
          cbFXCmd = track.fxcmds[row-1];
          cbFXParam = track.fxparams[row-1];
          break;
        case KeyEvent.VK_V:
          //paste note/vol/fx
          track.notes[row-1] = cbNote;
          track.volcmds[row-1] = cbVolCmd;
          track.volparams[row-1] = cbVolParam;
          track.fxcmds[row-1] = cbFXCmd;
          track.fxparams[row-1] = cbFXParam;
          updatePattern();
          break;
      }
    }
    tableView.changeSelection(row, col, false, false);
//    tableView.scrollRectToVisible(tableView.getCellRect(row, col, true));
    tableView.repaint();
  }//GEN-LAST:event_tableViewKeyPressed

  private void tableViewKeyReleased(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_tableViewKeyReleased
    evt.consume();
  }//GEN-LAST:event_tableViewKeyReleased

  private void jButton2ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton2ActionPerformed
    musicStart();
    music.playPattern(currentPatternIdx);
  }//GEN-LAST:event_jButton2ActionPerformed

  private void jButton3ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton3ActionPerformed
    if (recording)
      recordStop();
    else
      music.stopMusic();
    if (exporting) {
      musicEnded();
    }
  }//GEN-LAST:event_jButton3ActionPerformed

  private void bpmStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_bpmStateChanged
    if (currentPatternIdx == -1) return;
    music.song.patterns.get(currentPatternIdx).bpm = (Integer)bpm.getValue();
  }//GEN-LAST:event_bpmStateChanged

  private void jButton6ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton6ActionPerformed
    if (soundIdx != -1) {
      music.channelKeyUp(soundIdx);
//      soundIdx = -1;  //may still have a loop
    }
  }//GEN-LAST:event_jButton6ActionPerformed

  private void jButton5ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton5ActionPerformed
    if (soundIdx != -1) {
      music.channelStop(soundIdx);
      soundIdx = -1;
    }
  }//GEN-LAST:event_jButton5ActionPerformed

  private void jButton4ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton4ActionPerformed
    if (currentSampleIdx == -1) return;
    musicStart();
    if (soundIdx != -1) {
      music.channelStop(soundIdx);
      soundIdx = -1;
    }
    soundIdx = music.samplePlay(currentSampleIdx, 1.0f, 1.0f);
  }//GEN-LAST:event_jButton4ActionPerformed

  private void zoomItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_zoomItemStateChanged
    samples.revalidate();
    samples.repaint();
  }//GEN-LAST:event_zoomItemStateChanged

  private void iAddFileActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_iAddFileActionPerformed
    JFileChooser chooser = new JFileChooser();
    chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
    chooser.addChoosableFileFilter(new FileNameExtensionFilter("WAV", "wav"));
    chooser.setFileFilter(chooser.getChoosableFileFilters()[1]);  //use first added filefilter added
    chooser.setMultiSelectionEnabled(false);
    chooser.setCurrentDirectory(new File(MainPanel.currentPath));
    if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) return;
    int iidx = music.song.instruments.size();
    int sidx = music.song.samples.size();
    String name = "Instrument " + (iidx+1);
    music.addInstrument(name);
    music.addRegion(iidx, 0, 0x7f, 0x3c, sidx);
    if (!music.addSamples(name + "_" + 0, chooser.getSelectedFile().getAbsolutePath(), -1, -1, -1, -1, 0.0f)) {
      JFAWT.showError("Error", "Failed to load wav file");
      return;
    }
    updateSamples();
    sample.setSelectedIndex(sidx);
    updateSample();
    updateInstruments();
    instrument.setSelectedIndex(iidx);
    updateInstrument();
  }//GEN-LAST:event_iAddFileActionPerformed

  private void iAddLibraryActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_iAddLibraryActionPerformed
    String name = (String)library.getSelectedItem();
    if (name == null) return;
    int libidx = library.getSelectedIndex();
    if (libidx == -1) return;
    Library.Entry entry = Library.get(libidx);
    int iidx = music.song.instruments.size();
    int sidx = music.song.samples.size();
    switch (entry.type) {
      case WAV:
        music.addInstrument(name);
        music.addRegion(iidx, 0, 0x7f, 0x3c, sidx);
        if (!music.addSamples(name + "_" + 0, entry.fullPath, -1, -1, -1, -1, 0.0f)) {
          JFAWT.showError("Error", "Failed to load wav file");
          break;
        }
        break;
      case DLS:
        DLS dls = Library.getDLS(entry.dls_idx);
        music.addInstrument(name);
        int cnt = dls.getRegionsCount(name);
        for(int a=0;a<cnt;a++) {
          DLS.Instrument i = dls.getInstrument(name, a);
          if (i == null) return;
          int susStart = -1, susEnd = -1;
          if (i.loopStart != -1) {
            susStart = i.loopStart;
            susEnd = i.loopStart + i.loopLength;
          }
          DLS.Region region = dls.getRegion(name, a);
          music.addRegion(iidx, region.keyMin, region.keyMax, region.unityNote, sidx + a);
          float attenuation = 0.0f;  //region.attenuation;  //TODO???
          if (!music.addSamples(name + " " + (sidx+a), i.samples, -1, -1, susStart, susEnd, attenuation)) {
            JFAWT.showError("Error", "Failed to load wav file");
            break;
          }
        }
        break;
    }
    updateSamples();
    sample.setSelectedIndex(sidx);
    updateSample();
    updateInstruments();
    instrument.setSelectedIndex(iidx);
    updatePattern();
  }//GEN-LAST:event_iAddLibraryActionPerformed

  private void iRemoveActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_iRemoveActionPerformed
    if (currentInstrumentIdx == -1) return;
    if (!JFAWT.showConfirm("Delete Instrument?", "Are you sure?")) return;
    music.removeInstrument(currentInstrumentIdx);
    currentInstrumentIdx = -1;
    updateInstruments();
    updatePattern();
  }//GEN-LAST:event_iRemoveActionPerformed

  private void instrumentItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_instrumentItemStateChanged
    if (currentInstrumentIdx != -1) {
      saveInstrument();
    }
    currentInstrumentIdx = instrument.getSelectedIndex();
    updateInstrument();
  }//GEN-LAST:event_instrumentItemStateChanged

  private void loopItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_loopItemStateChanged
    if (loop.isSelected()) {
      if (samples.sample == null) {
        loop.setSelected(false);
        return;
      }
      if (samples.loopStart == -1) {
        int length = samples.sample.samples.length-1;
        samples.loopStart = (int)(length * 0.10f);
        samples.loopEnd = (int)(length * 0.90f);
      }
    } else {
      if (samples.sample == null) return;
//      samples.loopStart = -1;
//      samples.loopEnd = -1;
    }
    samples.showLoop = loop.isSelected();
    samples.repaint();
  }//GEN-LAST:event_loopItemStateChanged

  private void sustainItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_sustainItemStateChanged
    if (sustain.isSelected()) {
      if (samples.sample == null) {
        sustain.setSelected(false);
        return;
      }
      if (samples.sustainStart == -1) {
        int length = samples.sample.samples.length-1;
        samples.sustainStart = (int)(length * 0.05f);
        samples.sustainEnd = (int)(length * 0.95f);
      }
    } else {
      if (samples.sample == null) return;
//      samples.sustainStart = -1;
//      samples.sustainEnd = -1;
    }
    samples.showSustain = sustain.isSelected();
    samples.repaint();
  }//GEN-LAST:event_sustainItemStateChanged

  private void attenuationItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_attenuationItemStateChanged
    samples.showAttenuation = attenuation.isSelected();
    samples.repaint();
  }//GEN-LAST:event_attenuationItemStateChanged

  private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton1ActionPerformed
    if (recording)
      recordStop();
    else
      recordStart();
  }//GEN-LAST:event_jButton1ActionPerformed

  private void bpmKeyTyped(java.awt.event.KeyEvent evt) {//GEN-FIRST:event_bpmKeyTyped
    if (currentPatternIdx == -1) return;
    music.song.patterns.get(currentPatternIdx).bpm = (Integer)bpm.getValue();
  }//GEN-LAST:event_bpmKeyTyped

  private void jButton10ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton10ActionPerformed
    if (currentPatternIdx == -1) return;
    String newName = JFAWT.getString("Edit Name", (String)pattern.getSelectedItem());
    if (newName == null) return;
    music.song.patterns.get(currentPatternIdx).name = newName;
    int idx = currentPatternIdx;
    updatePatterns();
    pattern.setSelectedIndex(idx);
    updateSequence();
    updateSequenceList();
  }//GEN-LAST:event_jButton10ActionPerformed

  private void jButton9ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton9ActionPerformed
    if (currentInstrumentIdx == -1) return;
    String newName = JFAWT.getString("Edit Name", (String)instrument.getSelectedItem());
    if (newName == null) return;
    music.song.instruments.get(currentInstrumentIdx).name = newName;
    int idx = currentInstrumentIdx;
    updateInstruments();
    instrument.setSelectedIndex(idx);
    updatePattern();
  }//GEN-LAST:event_jButton9ActionPerformed

  private void sequenceValueChanged(javax.swing.event.ListSelectionEvent evt) {//GEN-FIRST:event_sequenceValueChanged
    currentSequenceIdx = sequence.getSelectedIndex();
  }//GEN-LAST:event_sequenceValueChanged

  private void jButton11ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton11ActionPerformed
    //move pattern up
    if (currentPatternIdx <= 0) return;
    if (music.song.patterns.size() < 2) return;
    Music.Pattern pat = music.song.patterns.remove(currentPatternIdx);
    music.song.patterns.add(currentPatternIdx - 1, pat);
    //update sequence
    for(int a=0;a<music.song.sequence.size();a++) {
      int idx = music.song.sequence.get(a);
      if (idx == currentPatternIdx) {
        music.song.sequence.set(a, idx - 1);
      } else if (idx == currentPatternIdx - 1) {
        music.song.sequence.set(a, idx + 1);
      }
    }
    int idx = currentPatternIdx - 1;
    updatePatterns();
    pattern.setSelectedIndex(idx);
    updatePattern();
    updateSequenceList();
    updateSequence();
  }//GEN-LAST:event_jButton11ActionPerformed

  private void jButton12ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton12ActionPerformed
    //move pattern down
    if (currentPatternIdx == -1) return;
    int patSize = music.song.patterns.size();
    if (patSize < 2) return;
    if (currentPatternIdx == patSize-1) return;
    Music.Pattern pat = music.song.patterns.remove(currentPatternIdx);
    music.song.patterns.add(currentPatternIdx + 1, pat);
    //update sequence
    for(int a=0;a<music.song.sequence.size();a++) {
      int idx = music.song.sequence.get(a);
      if (idx == currentPatternIdx) {
        music.song.sequence.set(a, idx + 1);
      } else if (idx == currentPatternIdx + 1) {
        music.song.sequence.set(a, idx - 1);
      }
    }
    int idx = currentPatternIdx + 1;
    updatePatterns();
    pattern.setSelectedIndex(idx);
    updatePattern();
    updateSequenceList();
    updateSequence();
  }//GEN-LAST:event_jButton12ActionPerformed

  private void sampleItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_sampleItemStateChanged
    if (currentSampleIdx != -1) {
      saveSample();
    }
    currentSampleIdx = sample.getSelectedIndex();
    updateSample();
  }//GEN-LAST:event_sampleItemStateChanged

  private void jButton13ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton13ActionPerformed
    if (currentInstrumentIdx == -1) return;
    EditRegion dialog = new EditRegion(null, true, music.song.instruments.get(currentInstrumentIdx).regions.get(currentRegionIdx), music, true);
    dialog.setVisible(true);
    updateInstrument();
  }//GEN-LAST:event_jButton13ActionPerformed

  private void regionsItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_regionsItemStateChanged
    currentRegionIdx = regions.getSelectedIndex();
  }//GEN-LAST:event_regionsItemStateChanged

  private void jButton14ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton14ActionPerformed
    if (currentInstrumentIdx == -1) return;
    Music.Region region = new Music.Region();
    EditRegion dialog = new EditRegion(null, true, region, music, false);
    dialog.setVisible(true);
    if (!dialog.accepted) return;
    music.song.instruments.get(currentInstrumentIdx).regions.add(region);
    updateInstrument();
  }//GEN-LAST:event_jButton14ActionPerformed

  private void jButton15ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton15ActionPerformed
    if (currentInstrumentIdx == -1) return;
    if (currentRegionIdx == -1) return;
    if (!JFAWT.showConfirm("Delete Region?", "Are you sure?")) return;
    music.song.instruments.get(currentInstrumentIdx).regions.remove(currentRegionIdx);
    updateInstrument();
  }//GEN-LAST:event_jButton15ActionPerformed

  private void jButton16ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton16ActionPerformed
    if (currentSampleIdx == -1) return;
    String newName = JFAWT.getString("Edit Name", music.song.samples.get(currentSampleIdx).name);
    if (newName == null) return;
    music.song.samples.get(currentSampleIdx).name = newName;
    updateSamples();
    updateSample();
  }//GEN-LAST:event_jButton16ActionPerformed

  private void jButton18ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton18ActionPerformed
    if (currentSampleIdx == -1) return;
    if (!JFAWT.showConfirm("Delete Sample?", "Are you sure?")) return;
    music.removeSamples(currentSampleIdx);
    updateSamples();
    updateSample();
  }//GEN-LAST:event_jButton18ActionPerformed

  private void jButton17ActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_jButton17ActionPerformed
    JFileChooser chooser = new JFileChooser();
    chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
    chooser.addChoosableFileFilter(new FileNameExtensionFilter("WAV", "wav"));
    chooser.setFileFilter(chooser.getChoosableFileFilters()[1]);  //use first added filefilter added
    chooser.setMultiSelectionEnabled(false);
    chooser.setCurrentDirectory(new File(MainPanel.currentPath));
    if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) return;
    int sidx = music.song.samples.size();
    File file = chooser.getSelectedFile();
    String name = file.getName();
    if (name.toLowerCase().endsWith(".wav")) {
      name = name.substring(0, name.length() - 4);
    }
    if (!music.addSamples(name, file.getAbsolutePath(), -1, -1, -1, -1, 0.0f)) {
      JFAWT.showError("Error", "Failed to load wav file");
      return;
    }
    updateSamples();
    sample.setSelectedIndex(sidx);
    updateSample();
  }//GEN-LAST:event_jButton17ActionPerformed

  // Variables declaration - do not modify//GEN-BEGIN:variables
  private javax.swing.JButton addPattern;
  private javax.swing.JCheckBox attenuation;
  private javax.swing.JSpinner bpm;
  private javax.swing.JTextArea comment;
  private javax.swing.Box.Filler filler1;
  private javax.swing.Box.Filler filler10;
  private javax.swing.Box.Filler filler11;
  private javax.swing.Box.Filler filler2;
  private javax.swing.Box.Filler filler3;
  private javax.swing.Box.Filler filler4;
  private javax.swing.Box.Filler filler5;
  private javax.swing.Box.Filler filler6;
  private javax.swing.Box.Filler filler7;
  private javax.swing.Box.Filler filler8;
  private javax.swing.Box.Filler filler9;
  private javax.swing.JPanel generalTab;
  private javax.swing.JButton iAddFile;
  private javax.swing.JButton iAddLibrary;
  private javax.swing.JButton iRemove;
  private javax.swing.JPanel instrTab;
  private javax.swing.JComboBox instrument;
  private javax.swing.JButton jButton1;
  private javax.swing.JButton jButton10;
  private javax.swing.JButton jButton11;
  private javax.swing.JButton jButton12;
  private javax.swing.JButton jButton13;
  private javax.swing.JButton jButton14;
  private javax.swing.JButton jButton15;
  private javax.swing.JButton jButton16;
  private javax.swing.JButton jButton17;
  private javax.swing.JButton jButton18;
  private javax.swing.JButton jButton2;
  private javax.swing.JButton jButton3;
  private javax.swing.JButton jButton4;
  private javax.swing.JButton jButton5;
  private javax.swing.JButton jButton6;
  private javax.swing.JButton jButton7;
  private javax.swing.JButton jButton8;
  private javax.swing.JButton jButton9;
  private javax.swing.JLabel jLabel1;
  private javax.swing.JLabel jLabel11;
  private javax.swing.JLabel jLabel13;
  private javax.swing.JLabel jLabel14;
  private javax.swing.JLabel jLabel2;
  private javax.swing.JLabel jLabel3;
  private javax.swing.JLabel jLabel4;
  private javax.swing.JLabel jLabel5;
  private javax.swing.JLabel jLabel6;
  private javax.swing.JLabel jLabel7;
  private javax.swing.JLabel jLabel8;
  private javax.swing.JLabel jLabel9;
  private javax.swing.JScrollPane jScrollPane1;
  private javax.swing.JScrollPane jScrollPane3;
  private javax.swing.JScrollPane jScrollPane4;
  private javax.swing.JToolBar jToolBar1;
  private javax.swing.JToolBar jToolBar2;
  private javax.swing.JToolBar jToolBar3;
  private javax.swing.JToolBar jToolBar4;
  private javax.swing.JToolBar jToolBar5;
  private javax.swing.JToolBar jToolBar6;
  private javax.swing.JToolBar jToolBar7;
  private javax.swing.JComboBox library;
  private javax.swing.JCheckBox loop;
  private javax.swing.JTextField name;
  private javax.swing.ButtonGroup noteTypes;
  private javax.swing.JComboBox pattern;
  private javax.swing.JPanel patternsTab;
  private javax.swing.JComboBox regions;
  private javax.swing.JButton removePattern;
  private javax.swing.JComboBox sample;
  private javax.swing.JScrollPane samplesPane;
  private javax.swing.JPanel samplesTab;
  private javax.swing.JButton seqAdd;
  private javax.swing.JButton seqDown;
  private javax.swing.JComboBox seqPattern;
  private javax.swing.JButton seqRemove;
  private javax.swing.JPanel seqTab;
  private javax.swing.JButton seqUp;
  private javax.swing.JList sequence;
  private javax.swing.JCheckBox sustain;
  private javax.swing.JTable tableView;
  private javax.swing.JTabbedPane tabs;
  private javax.swing.JComboBox zoom;
  // End of variables declaration//GEN-END:variables

  private DefaultTableModel tableModel = new DefaultTableModel() {
    public boolean isCellEditable(int row,int col) {
      if (row == 0 && col > 0) return true;
      return false;
    }
    public void setValueAt(Object value,int row,int col) {
      if (value == null) return;
      if (row == 0) {
        //DefaultCellEditor assumes entry was a string, so instead of a combobox
        JComboBox cb = (JComboBox)tableView.getValueAt(row, col);
        String str = (String)value;
        int idx = JF.atox(str.substring(0, 2));
        cb.setSelectedIndex(idx);
        music.song.patterns.get(currentPatternIdx).tracks.get(col-1).startInstrument = (byte)idx;
      } else {
        super.setValueAt(value, row, col);
      }
    }
  };
  private DefaultTableColumnModel colModel; // = new DefaultTableColumnModel();
  private Music music;
  private int currentPatternIdx = -1;
  private int currentSequenceIdx = -1;
  private int currentInstrumentIdx = -1;
  private int currentRegionIdx = -1;
  private int currentSampleIdx = -1;
  private Samples samples;
  private String filename;
  private int soundIdx = -1;
  private byte cbNote = -1, cbVolCmd, cbFXCmd;
  private int cbVolParam, cbFXParam;
  private MidiDevice midi;
  private Transmitter trans;
  private java.util.Timer timer;
  private boolean recording;
  private boolean recordingStarted;
  private boolean exporting;
  private MediaEncoder ffmpeg;
  private RandomAccessFile exportFile;

  private void reset() {
    music = new Music();
    music.reset();
    repaint();
    updatePatterns();
    updatePattern();
  }

  private void updatePatterns() {
    currentPatternIdx = -1;
    pattern.removeAllItems();
    int size = music.song.patterns.size();
    for(int a=0;a<size;a++) {
      pattern.addItem(music.song.patterns.get(a).name);
    }
  }

  private void updateSequence() {
    int idx = seqPattern.getSelectedIndex();
    seqPattern.removeAllItems();
    int cnt = pattern.getItemCount();
    if (idx >= cnt) idx = cnt - 1;
    for(int a=0;a<cnt;a++) {
      seqPattern.addItem(music.song.patterns.get(a).name);
    }
    seqPattern.setSelectedIndex(idx);
  }

  private void updatePattern() {
    updateSequence();
    tableModel.setRowCount(0);
    tableModel.setColumnCount(0);
    tableView.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
    if (currentPatternIdx >= music.song.patterns.size()) {
      currentPatternIdx = music.song.patterns.size() - 1;
      pattern.setSelectedIndex(currentPatternIdx);
    }
    if (currentPatternIdx == -1) {
      bpm.setValue(80);
      return;
    }
    Music.Pattern pattern = music.song.patterns.get(currentPatternIdx);
    bpm.setValue(pattern.bpm);
    int cnt = pattern.tracks.size();
    tableModel.setColumnCount(1 + cnt);
    ArrayList<Object> row = new ArrayList<Object>();
    row.clear();
    TableColumn col;
    col = colModel.getColumn(0);
//    col.setCellRenderer(new SimpleTableCellRenderer());
//    col.setHeaderRenderer(new SimpleTableCellRenderer());
//    col.setHeaderValue(null);
    row.add("");
    for(int t=0;t<cnt;t++) {
      Music.Track track = pattern.tracks.get(t);
      col = colModel.getColumn(t+1);
      col.setCellRenderer(new SimpleTableCellRenderer());
//      col.setHeaderRenderer(new SimpleTableCellRenderer());
//      col.setHeaderValue(new JComboBox());
      row.add(getTrackInstruments(track));
    }
    tableModel.addRow(row.toArray());
    for(int r=0;r<64;r++) {
      row.clear();
      row.add(String.format("%02d", r));
      for(int t=0;t<cnt;t++) {
        Music.Track track = pattern.tracks.get(t);
        row.add(new TableCell(getNoteVolFX(track, r)));
      }
      tableModel.addRow(row.toArray());
    }
    setTableWidths();
    tableView.repaint();
  }

  private void updateCell() {
    int col = tableView.getSelectedColumn();
    int row = tableView.getSelectedRow();
    if (col <= 0) return;
    if (row <= 0) return;
    if (currentPatternIdx == -1) return;
    Music.Track track = music.song.patterns.get(currentPatternIdx).tracks.get(col-1);
    Object obj = new TableCell(getNoteVolFX(track, row-1));
    tableView.setValueAt(obj, row, col);
    tableView.repaint();
  }

  private String getNoteVolFX(Music.Track track, int row) {
    return getNote(track.notes[row]) + " "
      + getVolCmd(track.volcmds[row]) //+ getHexParam(track.volparams[r])
      + " "
      + getFXCmd(track.fxcmds[row]); //+ getHexParam(track.fxparams[r])
  }

  private String keys[] = {"C-", "C#", "D-", "D#", "E-", "F-", "F#", "G-", "G#", "A-", "A#", "B-"};

  private String getNote(byte note) {
    if (note == -1) {
      return "...";
    }
    int octave = note / 12;
    int key = note % 12;
    return keys[key] + octave;
  }

  private String getVolCmd(int value) {
    switch (value) {
      case Music.VOLCMD_NONE: return "..";
      case Music.VOLCMD_SET_VOLUME: return "sv";
      case Music.VOLCMD_SET_VOL_VIBRATE_SPEED: return "vv";
      case Music.VOLCMD_SET_PAN_VIBRATE_SPEED: return "vp";
      case Music.VOLCMD_TREMOLO: return "tr";
      case Music.VOLCMD_PANBRELLO: return "pa";
      case Music.VOLCMD_SET_PANNING: return "sp";
      case Music.VOLCMD_SLIDE: return "vs";
      case Music.VOLCMD_PAN_SLIDE: return "ps";
    }
    return "??";
  }

  private String getFXCmd(int value) {
    switch (value) {
      case Music.FXCMD_NONE: return "..";
      case Music.FXCMD_PORTAMENTO_TO_NOTE: return "pn";
      case Music.FXCMD_SET_VIBRATE_SPEED: return "vs";
      case Music.FXCMD_VIBRATO: return "vb";
      case Music.FXCMD_PORTAMENTO: return "po";
      case Music.FXCMD_TREMOR: return "tr";
      case Music.FXCMD_SET_INSTRUMENT: return "si";
      case Music.FXCMD_DELAY_START: return "ds";
      case Music.FXCMD_SAMPLE_OFFSET: return "so";
      case Music.FXCMD_SET_BPM: return "sm";
      case Music.FXCMD_KEY_OFF: return "ko";
      case Music.FXCMD_PATTERN_BREAK: return "pb";
    }
    return "??";
  }

  private String getHexParam(int value) {
    //or float ???
    if (value == 0) return "........";
    return String.format("%08x", value);
  }

  private void setTableWidths() {
    int cnt = colModel.getColumnCount();
    for(int a=0;a<cnt;a++) {
      TableColumn col = colModel.getColumn(a);
      int width = 25;
      if (a > 0) width = 100;
      col.setResizable(false);
      col.setMinWidth(width);
      col.setMaxWidth(width);
      col.setPreferredWidth(width);
    }
  }

  private void editNote() {
    int col = tableView.getSelectedColumn();
    if (col <= 0) return;
    int row = tableView.getSelectedRow();
    if (row <= 0) return;
    row--; col--;
    if (currentPatternIdx == -1) return;
    Music.Pattern pattern = music.song.patterns.get(currentPatternIdx);
    Music.Track track = pattern.tracks.get(col);
    EditNote dialog = new EditNote(null, true, track.notes[row]
      ,track.volcmds[row], track.volparams[row]
      ,track.fxcmds[row], track.fxparams[row], music);
    dialog.setVisible(true);
    if (dialog.accepted) {
      track.notes[row] = dialog.note;
      track.volcmds[row] = dialog.volcmd;
      track.volparams[row] = dialog.volparam;
      track.fxcmds[row] = dialog.fxcmd;
      track.fxparams[row] = dialog.fxparam;
      updatePattern();
      tableView.repaint();
    }
  }

  private void updateSequenceList() {
    ArrayList<String> list = new ArrayList<String>();
    int cnt = music.song.sequence.size();
    for(int a=0;a<cnt;a++) {
      int idx = music.song.sequence.get(a);
      list.add("" + a + music.song.patterns.get(idx).name);
    }
    sequence.setListData(list.toArray(new String[0]));
  }

  private void updateInstruments() {
    saveInstrument();
    int idx = currentInstrumentIdx;
    currentInstrumentIdx = -1;
    instrument.removeAllItems();
    int cnt = music.song.instruments.size();
    if (cnt == 0) {
      return;
    }
    for(int a=0;a<cnt;a++) {
      instrument.addItem(music.song.instruments.get(a).name);
    }
    if (idx != -1) instrument.setSelectedIndex(idx);
  }

  private char row1[] = {'q' ,'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', '[', ']'};
  private char row2[] = {'a' ,'s', 'd', 'f', 'g', 'h', 'j', 'k', 'l', ';', '\'', '\r'};
  private char row3[] = {'z' ,'x', 'c', 'v', 'b', 'n', 'm', ',', '.', '/', '?', '?'};

  private void enterNote(char ch) {
    int row = tableView.getSelectedRow();
    int col = tableView.getSelectedColumn();
    if (row <= 0) return;
    if (col <= 0) return;
    if (currentPatternIdx == -1) return;
    TableCell cell = (TableCell)tableView.getValueAt(row, col);
    int sel = cell.getSelection();
    Music.Track track = music.song.patterns.get(currentPatternIdx).tracks.get(col-1);
    switch (sel) {
      case 0:  //note
        byte newnote = -2;
        if (ch == ' ') {
          newnote = -1;
        } else if (ch >= '0' && ch <= '9') {
          //change octave (if there is a note)
          if (track.notes[row-1] == -1) break;
          int octave = (ch - '0') * 12;
          int key = track.notes[row-1] % 12;
          track.notes[row-1] = (byte)(octave + key);
        } else {
          for(int a=0;a<12;a++) {
            if (row1[a] == ch) {
              newnote = (byte)((3 * 12) + a);
              break;
            }
            else if (row2[a] == ch) {
              newnote = (byte)((4 * 12) + a);
              break;
            }
            else if (row3[a] == ch) {
              newnote = (byte)((5 * 12) + a);
              break;
            }
          }
        }
        if (newnote != -2) track.notes[row-1] = newnote;
        break;
      case 1:  //volcmd
        if (ch == ' ') {
          track.volcmds[row-1] = 0;
          track.volparams[row-1] = 0;
        }
/*        if (ch >= '0' && ch <= '9') {
          track.volcmds[row-1] <<= 4;
          track.volcmds[row-1] |= (ch - '0');
        }
        else if (ch >= 'a' && ch <= 'f') {
          track.volcmds[row-1] <<= 4;
          track.volcmds[row-1] |= (ch - 'a' + 10);
        }
*/
        break;
/*
      case 2:  //volparam
        if (ch >= '0' && ch <= '9') {
          track.volparams[row-1] <<= 4;
          track.volparams[row-1] |= (ch - '0');
        }
        else if (ch >= 'a' && ch <= 'f') {
          track.volparams[row-1] <<= 4;
          track.volparams[row-1] |= (ch - 'a' + 10);
        }
        break;
*/
      case 2:  //fxcmd
        if (ch == ' ') {
          track.fxcmds[row-1] = 0;
          track.fxparams[row-1] = 0;
        }
/*
        if (ch >= '0' && ch <= '9') {
          track.fxcmds[row-1] <<= 4;
          track.fxcmds[row-1] |= (ch - '0');
        }
        else if (ch >= 'a' && ch <= 'f') {
          track.fxcmds[row-1] <<= 4;
          track.fxcmds[row-1] |= (ch - 'a' + 10);
        }
*/
        break;
/*
        case 4:  //fxparam
        if (ch >= '0' && ch <= '9') {
          track.fxparams[row-1] <<= 4;
          track.fxparams[row-1] |= (ch - '0');
        }
        else if (ch >= 'a' && ch <= 'f') {
          track.fxparams[row-1] <<= 4;
          track.fxparams[row-1] |= (ch - 'a' + 10);
        }
        break;
*/
    }
    updateCell();
    cell = (TableCell)tableView.getValueAt(row, col);
    cell.setSelection(sel);
  }

  public void selectTab(int idx) {
    tabs.setSelectedIndex(idx);
  }

  private void updateInstrument() {
    updateRegions();
  }

  private void saveInstrument() {

  }

  private void updateRegions() {
    regions.removeAllItems();
    if (currentInstrumentIdx == -1) return;
    Music.Instrument i = music.song.instruments.get(currentInstrumentIdx);
    if (i.regions == null) return;  //beta file
    for(int a=0;a<i.regions.size();a++) {
      Music.Region r = i.regions.get(a);
      regions.addItem("" + DLS.getKeyName(r.low) + " thru " + DLS.getKeyName(r.high));
    }
  }

  private void updateSample() {
    if (currentSampleIdx == -1) {
      loop.setSelected(false);
      sustain.setSelected(false);
      attenuation.setSelected(false);
      samples.setSample(null);
      samplesPane.repaint();
      return;
    }
    Music.Sample i = music.song.samples.get(currentSampleIdx);
    samples.setSample(i);
    samples.loopStart = i.loopStart;
    samples.loopEnd = i.loopEnd;
    loop.setSelected(i.loopStart != -1);
    samples.sustainStart = i.sustainStart;
    samples.sustainEnd = i.sustainEnd;
    sustain.setSelected(i.sustainStart != -1);
    samples.attenuation = i.attenuation;
    attenuation.setSelected(i.attenuation > 0.0f);
    samples.revalidate();
    samples.repaint();
  }

  public void saveSample() {
    if (currentSampleIdx == -1) return;
    Music.Sample s = music.song.samples.get(currentSampleIdx);
    if (loop.isSelected()) {
      s.loopStart = samples.loopStart;
      s.loopEnd = samples.loopEnd;
    } else {
      s.loopStart = -1;
      s.loopEnd = -1;
    }
    if (sustain.isSelected()) {
      s.sustainStart = samples.sustainStart;
      s.sustainEnd = samples.sustainEnd;
    } else {
      s.sustainStart = -1;
      s.sustainEnd = -1;
    }
    if (attenuation.isSelected())
      s.attenuation = samples.attenuation;
    else
      s.attenuation = 0.0f;
  }

  public void updateSamples() {
    saveSample();
    int idx = currentSampleIdx;
    currentSampleIdx = -1;
    sample.removeAllItems();
    int cnt = music.song.samples.size();
    if (cnt == 0) {
      return;
    }
    for(int a=0;a<cnt;a++) {
      sample.addItem(music.song.samples.get(a).name);
    }
    if (idx != -1) sample.setSelectedIndex(idx);
  }

  public int getZoom() {
    int idx = zoom.getSelectedIndex();
    switch (idx) {
      case 0: return 1;
      case 1: return 2;
      case 2: return 4;
      case 3: return 8;
      case 4: return 16;
      case 5: return 32;
      case 6: return 64;
      case 7: return 128;
    }
    return -1;
  }

  public JComboBox getTrackInstruments(final Music.Track track) {
    final JComboBox cb = new JComboBox();
    int cnt = music.song.instruments.size();
    if (cnt == 0) return cb;
    for(int a=0;a<cnt;a++) {
      cb.addItem(String.format("%02x", a) + ":" + music.song.instruments.get(a).name);
    }
    cb.setSelectedIndex(track.startInstrument);
    cb.addItemListener(new ItemListener(){
      public void itemStateChanged(ItemEvent e) {
        track.startInstrument = (byte)cb.getSelectedIndex();
      }
    });
    return cb;
  }

  private void loadLibrary() {
    library.removeAllItems();
    ArrayList<Library.Entry> list = Library.getList();
    for(int a=0;a<list.size();a++) {
      library.addItem(list.get(a).name);
    }
  }

  public void playSong() {
    musicStart();
    music.playSong(false);
  }

  public void playPattern() {
    musicStart();
    music.setListener(this);
    music.playSong(false);
  }

  public void stopSong() {
    music.stopMusic();
    if (exporting) {
      musicEnded();
    }
  }

  public void musicEnded() {
    JFLog.log("musicEnded");
    music.stop();
    if (exporting) {
      ffmpeg.stop();
      try {
        exportFile.close();
      } catch (Exception e) {
        JFLog.log(e);
      }
      exporting = false;
    }
  }

  public void musicSamples(short samples[]) {
    if (exporting) {
      ffmpeg.addAudio(samples);
    }
  }

  public void musicRow(int seq,int pat,int row) {
    final int _seq = seq;
    final int _pat = pat;
    final int _row = row;
    java.awt.EventQueue.invokeLater(new Runnable() {
      public void run() {
        if (_seq != -1) {
          if (currentSequenceIdx != _seq) {
            sequence.setSelectedIndex(_seq);
          }
        }
        if (_pat != -1) {
          if (currentPatternIdx != _pat) {
            pattern.setSelectedIndex(_pat);
          }
        }
        tableView.changeSelection(_row+1, 0, false, false);
      }
    });
  }

  private DefaultCellEditor editor;

  public TableCellEditor _getCellEditor(int row, int column) {
    int col = tableView.convertColumnIndexToModel(column);  //order of cols can change (not in this app though)
    JComboBox cb = (JComboBox)tableView.getValueAt(row, col);
    editor = new DefaultCellEditor(cb);
    editor.setClickCountToStart(1);
    return editor;
  }

  public Dimension getSamplesPaneSize() {
    return new Dimension(samplesPane.getWidth(), samplesPane.getHeight());
  }

  public void updateGeneral() {
    name.setText(music.song.name);
    comment.setText(music.song.comment);
  }

  public boolean load(String fn) {
    music = new Music();
    filename = fn;
    Music.Song song = music.load(fn);
    if (song == null) return false;
    music.load(song);
    updatePatterns();
    updatePattern();
    updateSequence();
    updateSequenceList();
    updateInstruments();
    updateInstrument();
    updateSamples();
    updateSample();
    updateGeneral();
    return true;
  }

  public boolean save() {
    if (filename == null) {
      JFileChooser chooser = new JFileChooser();
      chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
      chooser.addChoosableFileFilter(new FileNameExtensionFilter("Music Projects (*.mproj)", "mproj"));
      chooser.setFileFilter(chooser.getChoosableFileFilters()[1]);  //use first added filefilter added
      chooser.setMultiSelectionEnabled(false);
      chooser.setCurrentDirectory(new File(MainPanel.currentPath));
      if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) return false;
      File file = chooser.getSelectedFile();
      MainPanel.currentPath = file.getParent();
      if (MainPanel.currentPath == null) MainPanel.currentPath = JF.getUserPath() + "/Music";
      filename = file.getAbsolutePath();
      if (!filename.toLowerCase().endsWith(".mproj")) filename += ".mproj";
    }
    String title;
    title = filename;
    int idx = title.indexOf(".mproj");
    if (idx != -1) title = title.substring(0, idx);
    idx = title.lastIndexOf("/");
    if (idx != -1) title = title.substring(idx+1);
    idx = title.lastIndexOf("\\");
    if (idx != -1) title = title.substring(idx+1);
    music.song.name = name.getText();
    music.song.comment = comment.getText();
    MainPanel.This.setTitle(this, title);
    return music.save(filename);
  }

  public void closeSong() {
    music.close();
  }

  private void musicStart() {
    if (music.isRunning()) return;
    music.start(20, 1);
    music.setListener(this);
  }

  private void recordStart() {
    notes.clear();
    if (currentPatternIdx == -1) return;
    pattern.setEnabled(false);
    recordingStarted = false;
    recording = true;
    if (Settings.current.midiDevice.length() > 0) {
      //open midi device for recording
      loadMIDI(Settings.current.midiDevice);
    }
    int delay = (1000 * 60) / (Integer)bpm.getValue();
    timer = new java.util.Timer();
    timer.scheduleAtFixedRate(new TimerTask() {
      public void run() {
        java.awt.EventQueue.invokeLater(new Runnable() {
          public void run() {
            recordBeat();
          }
        });
      }
    }, delay, delay);
  }

  private void recordStop() {
    pattern.setEnabled(true);
    recording = false;
    if (timer != null) {
      timer.cancel();
      timer = null;
    }
    if (midi != null) {
      midi.close();
      midi = null;
    }
  }

  private class Note {
    int note;
    boolean down;
    int velocity;
  }

  private ArrayList<Note> notes = new ArrayList<Note>();
  private Object notesLock = new Object();
  private int noteTrack[] = new int[128];  //keep track of which track(col) note was played on

  private void recordBeat() {
    if (!recordingStarted) return;
    int col = tableView.getSelectedColumn();
    int row = tableView.getSelectedRow();
    int colMax = tableModel.getColumnCount();
    Note note;
    int rcol = col;
    while (!notes.isEmpty()) {
      synchronized(notesLock) {
        note = notes.remove(0);
      }
      if (note.down) {
        Music.Track track = music.song.patterns.get(currentPatternIdx).tracks.get(rcol-1);
        track.notes[row-1] = (byte)note.note;
        track.fxcmds[row-1] = 0;  //remove key off if set
        noteTrack[note.note] = rcol;
      } else {
        Music.Track track = music.song.patterns.get(currentPatternIdx).tracks.get(noteTrack[note.note]-1);
        track.fxcmds[row-1] = Music.FXCMD_KEY_OFF;
      }
      tableView.changeSelection(row, rcol, false, false);
      updateCell();
      if (note.down) rcol++;
      if (rcol == colMax) break;
    }
    row++;
    if (row == 64) {
      //end recording
      recordStop();
      return;
    }
    tableView.changeSelection(row, col, false, false);
  }

  private void loadMIDI(String midiName) {
    if (midi != null) {
      midi.close();
      midi = null;
    }
    MidiDevice.Info[] infos = MidiSystem.getMidiDeviceInfo();
    try {
      if (infos == null) throw new Exception("no devices found");
      for(int a=0;a<infos.length;a++) {
        MidiDevice midiDevice = MidiSystem.getMidiDevice(infos[a]);
        if (midiDevice.getMaxTransmitters() == 0) continue;
        String name = infos[a].getName();
        if (name.equals(midiName)) {
          midi = midiDevice;
          trans = midi.getTransmitter();
          trans.setReceiver(this);
          midi.open();
          JFLog.log("selected device:" + midi + "," + trans);
          return;
        }
      }
      if (midi == null) return;
    } catch (Exception e) {
      JFLog.log(e);
      return;
    }
  }

  //Receiver.close() - do nothing
  public void close() {}

  //Receiver.send() - receive MIDI message
  public void send(MidiMessage message, long timeStamp) {
    if (!(message instanceof ShortMessage)) return;
    //decode message
    ShortMessage sm = (ShortMessage)message;
    int cmd = sm.getCommand() & 0xff;
    int d1 = sm.getData1() & 0x7f;
    int d2 = sm.getData2() & 0x7f;
    String cmdstr = null;
    Note note = new Note();
    switch (cmd) {
      case 0x80:  //note off
        // note / velocity (zero)
        cmdstr = "keyUp";
        //d1 = key
        note.note = d1;
        note.down = false;
        synchronized(notesLock) {
          notes.add(note);
        }
        break;
      case 0x90:  //note on
        // note / velocity (0-127)
        cmdstr = "keyDn";
        //d1 = note
        //d2 = velocity
        note.note = d1;
        note.velocity = d2;
        note.down = true;
        synchronized(notesLock) {
          notes.add(note);
        }
        if (!recordingStarted) recordingStarted = true;
//        if (!velocity.isSelected()) d2 = 0x7f;
        break;
      case 0xb0:  //control change
        // control / value (0-127)
        // controls : 1 = modulation : 7 = volume : 4a = ch1 : 47 = ch2 : 49 = ch3 : 48 = ch4
        cmdstr = " ctrl";
        break;
      case 0xe0:  //pitch wheel
        cmdstr = "pitch";
        // zero / value (0-127)
        break;
    }
    JFLog.log(String.format("%s:%02x:%02x:%02x", cmdstr, cmd, d1, d2));
  }

  public void insertRow() {
    int row = tableView.getSelectedRow();
    int col = tableView.getSelectedColumn();
    if (row <= 0) return;
    if (col <= 0) return;
    if (currentPatternIdx == -1) return;
    row--;
    col--;
    Music.Pattern pat = music.song.patterns.get(currentPatternIdx);
    int nTracks = pat.tracks.size();
    for(int t=0;t<nTracks;t++) {
      Music.Track track = pat.tracks.get(t);
      for(int r=63;r>row;r--) {
        track.notes[r] = track.notes[r-1];
        track.volcmds[r] = track.volcmds[r-1];
        track.volparams[r] = track.volparams[r-1];
        track.fxcmds[r] = track.fxcmds[r-1];
        track.fxparams[r] = track.fxparams[r-1];
      }
      track.notes[row] = -1;
      track.volcmds[row] = 0;
      track.volparams[row] = 0;
      track.fxcmds[row] = 0;
      track.fxparams[row] = 0;
    }
    updatePattern();
  }

  public void deleteRow() {
    int row = tableView.getSelectedRow();
    int col = tableView.getSelectedColumn();
    if (row <= 0) return;
    if (col <= 0) return;
    row--;
    col--;
    if (currentPatternIdx == -1) return;
    Music.Pattern pat = music.song.patterns.get(currentPatternIdx);
    int nTracks = pat.tracks.size();
    for(int t=0;t<nTracks;t++) {
      Music.Track track = pat.tracks.get(t);
      for(int r=row;r<64;r++) {
        track.notes[r] = track.notes[r+1];
        track.volcmds[r] = track.volcmds[r+1];
        track.volparams[r] = track.volparams[r+1];
        track.fxcmds[r] = track.fxcmds[r+1];
        track.fxparams[r] = track.fxparams[r+1];
      }
      track.notes[63] = -1;
      track.volcmds[63] = 0;
      track.volparams[63] = 0;
      track.fxcmds[63] = 0;
      track.fxparams[63] = 0;
    }
    updatePattern();
  }

  public void dupPattern() {
    if (currentPatternIdx == -1) return;
    Music.Pattern oldpat = music.song.patterns.get(currentPatternIdx);
    music.addPattern(oldpat.tracks.size());
    int size = music.song.patterns.size();
    String name = "Verse " + size;
    int idx = size - 1;
    Music.Pattern newpat = music.song.patterns.get(idx);
    newpat.name = name;
    newpat.bpm = oldpat.bpm;
    int nTracks = oldpat.tracks.size();
    for(int t=0;t<nTracks;t++) {
      Music.Track oldtrack = oldpat.tracks.get(t);
      Music.Track newtrack = newpat.tracks.get(t);
      for(int r=0;r<64;r++) {
        newtrack.notes[r] = oldtrack.notes[r];
        newtrack.volcmds[r] = oldtrack.volcmds[r];
        newtrack.volparams[r] = oldtrack.volparams[r];
        newtrack.fxcmds[r] = oldtrack.fxcmds[r];
        newtrack.fxparams[r] = oldtrack.fxparams[r];
      }
    }
    pattern.addItem(name);
    pattern.setSelectedIndex(idx);
    updatePattern();
  }

  public void export() {
    JFileChooser chooser = new JFileChooser();
    chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
    FileNameExtensionFilter wav = new FileNameExtensionFilter("WAV (*.wav)", "wav");
    JFLog.log("wav=" + wav);
    chooser.addChoosableFileFilter(wav);
    FileNameExtensionFilter mp3 = new FileNameExtensionFilter("MP3 (*.mp3)", "mp3");
    JFLog.log("mp3=" + mp3);
    chooser.addChoosableFileFilter(mp3);
    chooser.setFileFilter(chooser.getChoosableFileFilters()[1]);  //use first added filefilter added
    chooser.setMultiSelectionEnabled(false);
    chooser.setCurrentDirectory(new File(MainPanel.currentPath));
    if (chooser.showSaveDialog(this) != JFileChooser.APPROVE_OPTION) return;
    String file = chooser.getSelectedFile().getAbsolutePath();
    String ext;
    int idx = file.lastIndexOf('.');
    if (idx == -1) {
      JFLog.log("fileFilter=" + chooser.getFileFilter());
      if (chooser.getFileFilter() == wav) {
        file += ".wav";
        ext = "wav";
      } else if (chooser.getFileFilter() == mp3) {
        ext = "mp3";
        file += ".mp3";
      } else {
        //assume wav
        file += ".wav";
        ext = "wav";
      }
    } else {
      ext = file.substring(idx+1).toLowerCase();
      if (!ext.equals("wav") && !ext.equals("mp3")) {
        //add ext
        JFLog.log("fileFilter=" + chooser.getFileFilter());
        if (chooser.getFileFilter() == wav) {
          ext = "wav";
          file += ".wav";
        } else if (chooser.getFileFilter() == mp3) {
          ext = "mp3";
          file += ".mp3";
        } else {
          //assume wav
          file += ".wav";
          ext = "wav";
        }
      }
    }
    try {
      exportFile = new RandomAccessFile(file, "rw");
    } catch (Exception e) {
      JFLog.log(e);
      JFAWT.showError("Error", "Failed to create export file");
      return;
    }
    exporting = true;
    ffmpeg = new MediaEncoder();
    if (ext.equals("mp3")) {
      String bitRate = JFAWT.getString("Enter bitrate (32-512)", "128");
      if (bitRate == null) bitRate = "128";
      int bits = JF.atoi(bitRate);
      if (bits < 32) bits = 32;
      if (bits > 512) bits = 512;
      ffmpeg.setAudioBitRate(bits * 1024);
    }
    ffmpeg.start(this, -1, -1, -1, 2, 44100, ext, false, true);
    playSong();
  }

//MediaCoderIO interface

  public int read(MediaCoder coder, byte[] bytes) {
    return 0;
  }

  public int write(MediaCoder coder, byte[] bytes) {
    try {
      exportFile.write(bytes);
    } catch (Exception e) {
      JFLog.log(e);
    }
    return bytes.length;
  }

  public long seek(MediaCoder coder, long pos, int how) {
    try {
      switch (how) {
        case MediaCoder.SEEK_SET:
          exportFile.seek(pos);
          break;
        case MediaCoder.SEEK_CUR:
          exportFile.seek(exportFile.getFilePointer() + pos);
          break;
        case MediaCoder.SEEK_END:
          exportFile.seek(exportFile.length() + pos);
          break;
      }
    } catch (Exception e) {
      JFLog.log(e);
    }
    return pos;
  }
}
